"""
glboard -- an OpenGL Go board

This file is part of Gimme Go <http://code.google.com/p/gimmego/>,
and is free software, under the terms of the GNU GPL.

Ken Harris <kengruven@gmail.com>
"""

from __future__ import with_statement
from withopengl import gl_matrix, gl_group, gl_list  # experimental: gl_features

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

import sys

import handicap
from point import Point
from board import MARKER_X, MARKER_TRIANGLE, MARKER_CIRCLE, MARKER_SQUARE

EMPTY, BLACK, WHITE = range(3)  # WHERE TO PUT THIS?  (point.py?)

class Position(object):
    """a particular position of the go board.  this includes the board size
    and position of stones, of course.  it also includes the sgf markers
    (labels, triangles, lines, etc.) on particular stones.

    in the future it may include more sophisticated things like whose turn
    it is, the current score, and so on.

    these are generated (somewhat lazily -- tbd) from the game tree, and
    are what the graphical displays present.
    """

    def __init__(self):
        # for now, until i hook it up, i'll just display a static position:
        position_str =  ['...X..........O....',
                         '.OOOX..OXX...OX....',
                         'O.OXX.XOO.XO.OX....',
                         'XOO.OX.O.OOX..OXX..',
                         '...X.X...X.X..OXXX.',
                         'XOO.OXOOOXOXOOX.XO.',
                         '.XO.X.XXOO.OX.OXO..',
                         '.XX.OX.X..O.O..X...',
                         '.O.XOOX.XOO.XXXXX..',
                         '...XXXXXXOXX.OOO.X.',
                         '..OOX...XXOOO.XOO..',
                         '...XX.OOXOOOX.X....',
                         '..X...OXXXOXXO.OXO.',
                         '.XOOO.OXXXO....OOX.',
                         '..XXOOXXOOOXXX.O.O.',
                         '.XXO..OXOOOOOXO.OX.',
                         '.XOOOO.OXXXXOX.OXX.',
                         'OOXO..O.OXX.X.XOOX.',
                         '.X...O.OOOXX.X.....']
        mapping = {'X': BLACK, 'O': WHITE, '.': EMPTY}

        # put it into self.stones, a 2d array of (BLACK, WHITE, EMPTY).
        self.stones = []
        for row_str in position_str:
            self.stones += [[ mapping[char] for char in row_str ]]

        self.markers = {}  # {point: str | MARKER_*} -- (dict ok?)

        # some dummy markers to test...
        self.markers[Point(0, 1)] = MARKER_X
        self.markers[Point(1, 1)] = MARKER_SQUARE
        self.markers[Point(3, 1)] = MARKER_TRIANGLE
        self.markers[Point(3, 2)] = MARKER_CIRCLE
        self.markers[Point(2, 1)] = MARKER_CIRCLE  # white?
##        for i in range(19):
##            for j in range(19):
##                self.markers[Point(i, j)] = MARKER_X

        """
        TODO: add other sgf features here:
        - M[] -- ??
        - L (list of point) [A, B, C, ... -- ??]
        - LB (point -> str)
        - SL (point)
        - DD (area? -- shaded)
        - AR (point->point) (arrow)
        - LN (point->point) (line)

        these fall into the categories:
        - symbol-at-point: (point -> symbol)
        - text-at-point (same thing?)
        - point-to-point (point,point -> type)
        - area effect? (point1, point2 -> type)
        """

#
# constants!
#
SIZE = 19  # TODO: not really a const... -- watch for SZ-change events on root node!
STRETCH = 1.1
BOARD_THICKNESS = 4.0
HOSHI_SIDES = 12
HOSHI_RADIUS = 0.04  # BUT: why do i *2 this, then?
STONE_SIDES = 16  # also, for their shadows, to match
STONE_VERTICAL_SIDES = 3  # doesn't need to be nearly as high as STONE_SIDES
STONE_THICKNESS = 0.3  # where diameter = 1.0

class GLBoard(object):
    """
    coordinate system: z+ is up.  x+ is right, and y+ is away from you.
    z=0 is the surface of the board (it goes down to z=-BOARD_THICKNESS),
    and x,y=0,0 is the center of it.  a stone has diameter 1;
    points in the x-direction are distance 1 apart; in the y-direction,
    they're distance STRETCH apart.  stones are squashed spheres, with
    height STONE_THICKNESS.
    
    """

    def __init__(self):
        self.theta = 0.0  # angle at which the board is rotated
        self.click_pt = (0, 0)

        # for now, until i hook it up, i'll just display a static position:
        self.position = Position()

    def _opposite_color(self, pt):
        # FIXME: pt.size-pt.row-1 -- WHY?  where did i screw up?
        if self.position.stones[pt.size - pt.row - 1][pt.col] == BLACK:
            return (1.0, 1.0, 1.0)
        else:
            return (0.0, 0.0, 0.0)

    def draw_stone(self, x, y, color):
        # draw a sphere
        with gl_matrix():
            glScalef(1.0, 1.0, STONE_THICKNESS)
            glTranslatef(-SIZE / 2.0 + 0.5, -SIZE * STRETCH / 2.0 + 0.5, 0.0)
            glTranslatef(x, y * STRETCH, 0.5)
            if color == WHITE:
                glColor3f(0.5, 0.5, 0.5)
            else:
                glColor3f(0.05, 0.05, 0.05)
            glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, (0.2, 0.2, 0.2, 1.0))
            glutSolidSphere(0.5, STONE_SIDES, STONE_VERTICAL_SIDES)

        # now draw a shadow under it
        with gl_matrix():
            glTranslatef(-SIZE / 2.0 + 0.5, -SIZE * STRETCH / 2.0 + 0.5, 0.0)
            glTranslatef(x, y * STRETCH, 0.01)  # HACK: more polygon offset!
            glColor4f(0.0, 0.0, 0.0, 0.3)
            quad = gluNewQuadric()
            gluDisk(quad, 0.0, 0.5, STONE_SIDES, 1)  # inner, outer, slices=edges, loops
        # TODO: make its position (and size!) accurate, given the light-point

    def draw_x(self, pt):
        glColor3f(*self._opposite_color(pt))
        with gl_matrix():
            glTranslatef(0.0, 0.0, STONE_THICKNESS)
            glTranslatef(-SIZE / 2.0 + 0.5, -SIZE * STRETCH / 2.0 + 0.5, 0.0)
            glLineWidth(3)
            with gl_group(GL_LINES):
                size = 0.3

                glVertex2f(pt.col - size, (pt.row - size) * STRETCH)
                glVertex2f(pt.col + size, (pt.row + size) * STRETCH)

                glVertex2f(pt.col - size, (pt.row + size) * STRETCH)
                glVertex2f(pt.col + size, (pt.row - size) * STRETCH)

    def draw_square(self, pt):
        glColor3f(*self._opposite_color(pt))
        with gl_matrix():
            glTranslatef(0.0, 0.0, STONE_THICKNESS)
            glTranslatef(-SIZE / 2.0 + 0.5, -SIZE * STRETCH / 2.0 + 0.5, 0.0)  # BUG: 0.5 needs to be stretched, too!  (also, a couple places above)
            glLineWidth(3)
            with gl_group(GL_LINE_LOOP):
                # FIXME: DRY!  0.5...  (also, in draw_x ... which itself is another DRY!)
                size = 0.3
                glVertex2f(pt.col - size, (pt.row - size) * STRETCH)
                glVertex2f(pt.col + size, (pt.row - size) * STRETCH)
                glVertex2f(pt.col + size, (pt.row + size) * STRETCH)
                glVertex2f(pt.col - size, (pt.row + size) * STRETCH)
                glVertex2f(pt.col - size, (pt.row - size) * STRETCH)

    def draw_triangle(self, pt):
        glColor3f(*self._opposite_color(pt))
        with gl_matrix():
            glTranslatef(0.0, 0.0, STONE_THICKNESS)
            glTranslatef(-SIZE / 2.0 + 0.5, -SIZE * STRETCH / 2.0 + 0.5, 0.0)  # BUG: 0.5 needs to be stretched, too!  (also, a couple places above)
            glLineWidth(3)
            with gl_group(GL_LINE_LOOP):
                # FIXME: sizes aren't quite right...
                size = 0.5
                glVertex2f(pt.col - size / 2.0, (pt.row - size / 2.0) * STRETCH)
                glVertex2f(pt.col + size / 2.0, (pt.row - size / 2.0) * STRETCH)
                glVertex2f(pt.col, (pt.row + size / 2.0) * STRETCH)
                glVertex2f(pt.col - size / 2.0, (pt.row - size / 2.0) * STRETCH)

    def draw_circle(self, pt):
        glColor3f(*self._opposite_color(pt))
        with gl_matrix():
            glTranslatef(0.0, 0.0, STONE_THICKNESS)
            glTranslatef(-SIZE / 2.0 + 0.5, -SIZE * STRETCH / 2.0 + 0.5, 0.0)  # BUG: 0.5 needs to be stretched, too!  (also, a couple places above)
            glTranslatef(pt.col, pt.row * STRETCH, 0)
            quad = gluNewQuadric()
            r = 0.4 ## ??
            thickness = 0.05  # i specify pixels (i think) for the others ... ick!
            gluDisk(quad, r - 0.1, r, HOSHI_SIDES, 1)  # inner, outer, slices=edges, loops


    def draw_board(self):
        # wood
        glColor3f(228/255.0, 186/255.0, 101/255.0)  # brownish-tanish
        with gl_matrix():
            glScalef(SIZE, SIZE * STRETCH, BOARD_THICKNESS)
            glTranslatef(0.0, 0.0, -0.5)
            glutSolidCube(1.0)

        # lines
        glColor3f(0.0, 0.0, 0.0)
        glLineWidth(1.5)
        with gl_matrix():
            glTranslatef(-SIZE / 2.0 + 0.5, -SIZE * STRETCH / 2.0 + 0.5, 0.0)
            glTranslatef(0.0, 0.0, 0.01)  # HACK: poor man's glPolygonOffset
            with gl_group(GL_LINES):
                for x in range(SIZE):
                    glVertex2f(0, x*STRETCH)
                    glVertex2f(SIZE-1, x*STRETCH)

                    glVertex2f(x, 0)
                    glVertex2f(x, (SIZE-1)*STRETCH)

            # hoshi
            quad = gluNewQuadric()
            glTranslatef(0.0, 0.0, 0.01)  # HACK: more glPolygonOffset hacks
            for pt in handicap.get_handicap(SIZE, handicap.get_max_handicap(SIZE)):
                with gl_matrix():
                    glTranslatef(pt.col, pt.row * STRETCH, 0)
                    gluDisk(quad, 0.0, HOSHI_RADIUS*2, HOSHI_SIDES, 1)  # inner, outer, slices=edges, loops

    def display(self):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glLoadIdentity()  # clear the matrix 
        # viewing transformation 
        gluLookAt(0.0, -5, 15.0,  # eyeX,Y,Z
                  0.0, 0.0, 0.0,  # centerX,Y,Z
                  0.0, 0.0, 1.0)  # upX,Y,Z
    #??    glScalef(1.0, 2.0, 1.0)      # modeling transformation 

        glRotatef(self.theta, 0.0, 0.0, 1.0)

        self.draw_board()
        for y, row in enumerate(self.position.stones):
            for x, stone in enumerate(row):
                if stone != EMPTY:
                    self.draw_stone(x, SIZE - y - 1, stone)

        # markers!
        for pt,mark in self.position.markers.items():
            if mark == MARKER_X:
                self.draw_x(pt)
            elif mark == MARKER_SQUARE:
                self.draw_square(pt)
            elif mark == MARKER_TRIANGLE:
                self.draw_triangle(pt)
            elif mark == MARKER_CIRCLE:
                self.draw_circle(pt)
            else:
                print "WRITEME: don't know how to handle mark=%s yet" % mark

    ##    glFlush()
        glutSwapBuffers()  # to be overridden, e.g., gtkglext = gdk_gl_drawable_swap_buffers()

    def reshape(self, w, h):
        aspect = w / float(h)

        glViewport(0, 0, w, h)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()

        left = -1.0
        right = 1.0
        top = 1.0
        bottom = -1.0

        if aspect < 1.0:
            bottom /= aspect
            top /= aspect
        else:
            left *= aspect
            right *= aspect
        glFrustum(left, right, bottom, top, 1.0, 30.0)  # ..., zNear, zFar

        glMatrixMode(GL_MODELVIEW)

    def keyboard(self, key, x, y):
        if key == chr(27):
            sys.exit(0)

    def on_mouse(self, button, state, x, y):
        # button is 0, 1, 2
        # state is 0=pressed, 1=released
        # x,y is the pixel position, 0,0=left,top

        self.click_pt = (x, y)
        self.click_button = button

    def on_motion(self, x, y):
        if self.click_button == 1:
            self.theta += (self.click_pt[0] - x) / 5.0  # USE REAL WIDTH HERE?
            self.click_pt = (x, y)

            self.display()

    def init(self):
        # set up lights
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_COLOR_MATERIAL)

        # set up antialiasing
        glEnable(GL_LINE_SMOOTH)
        glEnable(GL_POLYGON_SMOOTH)
        glEnable(GL_BLEND)
        # glDisable(GL_DITHER)  # on by default ... "assume rgba capabilities"?
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)

        # ??
        glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
        glFrontFace(GL_CW);
        glDisable(GL_CULL_FACE);

        glClearColor(116/255.0, 157/255.0, 203.0, 0.0)  # blue-gray... is this the correct way to do it?
        ##glClearDepth??
        glShadeModel(GL_SMOOTH)

# ----------------------------------------

if __name__ == '__main__':
    board = GLBoard()

    # glut-specific init
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH)
    glutInitWindowSize(500, 500)
    glutInitWindowPosition(100, 100)
    glutCreateWindow('cube')

    # GL init
    board.init()

    # glut callbacks
    glutDisplayFunc(board.display)
    glutReshapeFunc(board.reshape)
    glutKeyboardFunc(board.keyboard)
    glutMouseFunc(board.on_mouse)
    glutMotionFunc(board.on_motion)
    # glutPassiveMotionFunc(on_passive_motion)  # moved while no buttons down

    # go!
    glutMainLoop()
